package edu.kufpg.armatus.console;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.Parcel;
import android.os.Parcelable;
import com.google.common.base.Function;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.SortedSetMultimap;
import com.google.common.collect.TreeMultimap;
import edu.kufpg.armatus.DeviceConstants;
import edu.kufpg.armatus.Prefs;
import edu.kufpg.armatus.data.Command;
import edu.kufpg.armatus.data.CommandInfo;
import edu.kufpg.armatus.data.CommandResponse;
import edu.kufpg.armatus.data.Complete;
import edu.kufpg.armatus.data.Completion;
import edu.kufpg.armatus.data.History;
import edu.kufpg.armatus.data.HistoryCommand;
import edu.kufpg.armatus.data.Token;
import edu.kufpg.armatus.networking.BluetoothUtils;
import edu.kufpg.armatus.networking.HermitBluetoothReceiveRequest;
import edu.kufpg.armatus.networking.HermitBluetoothSendRequest;
import edu.kufpg.armatus.networking.HermitHttpServerRequest;
import edu.kufpg.armatus.networking.HermitHttpServerRequest.HttpRequest;
import edu.kufpg.armatus.networking.InternetUtils;
import edu.kufpg.armatus.util.JsonUtils;
import edu.kufpg.armatus.util.ParcelUtils;
import edu.kufpg.armatus.util.StringUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Arrays;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
public class HermitClient implements Parcelable {
public static int NO_TOKEN = -1;
private static final String HISTORY_FILENAME = "/history.txt";
private ConsoleActivity mConsole;
private ProgressDialog mProgress;
private RequestName mDelayedRequestName = RequestName.NULL;
private String mServerUrl;
private Bundle mTempBundle = new Bundle();
private Token mToken;
private final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
String input = (String) msg.obj;
if (input != null) {
mConsole.appendErrorResponse(input);
}
}
};
public HermitClient(ConsoleActivity console) {
mConsole = console;
}
public void handleInput(String input) {
mHandler.obtainMessage(-1, input).sendToTarget();
}
public void completeInput(final String input) {
if (isTokenAcquired(false)) {
if (isNetworkConnected(RequestName.COMPLETE)) {
Complete complete = new Complete(mToken.getUser(), input);
newCompleteInputRequest().execute(mServerUrl + "/complete", complete.toString());
} else {
mTempBundle.putString("input", input);
}
} else {
mConsole.attemptInputCompletion(null);
}
}
public void connect(String serverUrl) {
mServerUrl = serverUrl;
if (isNetworkConnected(RequestName.CONNECT)) {
newConnectRequest().execute(mServerUrl + "/connect");
}
}
public void fetchCommands() {
if (isNetworkConnected(RequestName.COMMANDS) && isTokenAcquired(true)) {
newFetchCommandsRequest().execute(mServerUrl + "/commands");
}
}
public void fetchHistory() {
if (isNetworkConnected(RequestName.HISTORY) && isTokenAcquired(true)) {
newSaveHistoryRequest().execute(mServerUrl + "/history", mToken.toString());
}
}
public void loadHistory() {
if (isNetworkConnected(RequestName.HISTORY) && isTokenAcquired(false)) {
String path = "";
if (Prefs.isHistoryDirCustom(mConsole)) {
path = Prefs.getHistoryDir(mConsole);
} else {
path = DeviceConstants.CACHE_DIR;
}
final File file = new File(path + HISTORY_FILENAME);
if (file.exists()) {
try {
History history = new History(JsonUtils.openJsonFile(file.getAbsolutePath()));
loadHistoryCommands(history.getCommands());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (JSONException e) {
mConsole.appendErrorResponse("ERROR: saved history corrupted.");
e.printStackTrace();
}
} else {
mConsole.appendErrorResponse("ERROR: no saved history exists.");
}
} else {
mConsole.appendErrorResponse("ERROR: connect before attempting to load history.");
}
}
public void runCommand(String input, int charsPerLine) {
String[] inputs = input.trim().split(StringUtils.WHITESPACE);
mConsole.addUserInputEntry(input);
if (CustomCommandDispatcher.isCustomCommand(inputs[0])) {
if (inputs.length == 1) {
CustomCommandDispatcher.runCustomCommand(mConsole, inputs[0]);
} else {
CustomCommandDispatcher.runCustomCommand(mConsole, inputs[0],
Arrays.copyOfRange(inputs, 1, inputs.length));
}
} else {
//Hardcode Bluetooth testing for now since I'm lazy
if (Prefs.isBluetoothSource(mConsole)) {
if (input.equals("btconnect") && !BluetoothUtils.isBluetoothConnected(mConsole)) {
new HermitBluetoothReceiveRequest(mConsole).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} else if (isNetworkConnected(RequestName.COMMAND) && BluetoothUtils.isBluetoothConnected(mConsole)) {
String cleanInput = StringUtils.noCharWrap(input);
new HermitBluetoothSendRequest(mConsole).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, cleanInput);
} else if (BluetoothUtils.isBluetoothConnected(mConsole)) {
mTempBundle.putString("input", input);
mTempBundle.putInt("charsPerLine", charsPerLine);
}
} else {
if (isNetworkConnected(RequestName.COMMAND) && isTokenAcquired(true)) {
String cleanInput = StringUtils.noCharWrap(input);
Command command = new Command(mToken, cleanInput, charsPerLine);
if (inputs[0].equals("abort") || inputs[0].equals("resume")) {
newRunAbortResumeRequest().execute(mServerUrl + "/command", command.toString());
} else {
newRunCommandRequest(cleanInput).execute(mServerUrl + "/command", command.toString());
}
} else {
mTempBundle.putString("input", input);
mTempBundle.putInt("charsPerLine", charsPerLine);
}
}
}
}
private HermitHttpServerRequest<List<Completion>> newCompleteInputRequest() {
return new HermitHttpServerRequest<List<Completion>>(mConsole, HttpRequest.POST) {
@Override
protected void onPreExecute() {
super.onPreExecute();
getActivity().disableInput(false);
}
@Override
protected List<Completion> onResponse(String response) {
JSONObject insertNameHere;
try {
insertNameHere = new JSONObject(response);
JSONArray completions = insertNameHere.getJSONArray("completions");
ImmutableList.Builder<Completion> completionsBuilder = ImmutableList.builder();
for (int i = 0; i < completions.length(); i++) {
completionsBuilder.add(new Completion(completions.getJSONObject(i)));
}
return completionsBuilder.build();
} catch (JSONException e) {
e.printStackTrace();
return null;
}
}
@Override
protected void onCancelled(List<Completion> error) {
String newErrorMessage = getErrorMessage();
setErrorMessage(null);
if (newErrorMessage != null && getActivity() != null) {
getActivity().addErrorResponseEntry(newErrorMessage);
}
super.onCancelled(error);
}
@Override
protected void onPostExecute(List<Completion> completions) {
super.onPostExecute(completions);
SortedSet<String> suggestions = new TreeSet<String>();
suggestions.addAll(Collections2.transform(completions, new Function<Completion, String>() {
@Override
public String apply(Completion input) {
return input.getReplacement();
}
}));
getActivity().attemptInputCompletion(suggestions);
}
};
}
private HermitHttpServerRequest<Token> newConnectRequest() {
return new HermitHttpServerRequest<Token>(mConsole, HttpRequest.POST) {
@Override
protected void onPreExecute() {
super.onPreExecute();
getActivity().setProgressBarVisibility(false);
showProgressDialog(getActivity(), this, "Connecting...");
}
@Override
protected void onActivityDetached() {
if (mProgress != null) {
mProgress.dismiss();
mProgress = null;
}
}
@Override
protected void onActivityAttached() {
if (mProgress == null) {
showProgressDialog(getActivity(), this, "Connecting...");
}
}
@Override
protected Token onResponse(String response) {
try {
return new Token(new JSONObject(response));
} catch (JSONException e) {
e.printStackTrace();
return null;
}
}
@Override
protected void onCancelled(Token error) {
super.onCancelled(error);
dismissProgressDialog();
}
@Override
protected void onPostExecute(Token token) {
super.onPostExecute(token);
mToken = token;
dismissProgressDialog();
getActivity().updateInput();
fetchCommands();
}
};
}
private HermitHttpServerRequest<List<CommandInfo>> newFetchCommandsRequest() {
return new HermitHttpServerRequest<List<CommandInfo>>(mConsole, HttpRequest.GET) {
@Override
protected void onPreExecute() {
super.onPreExecute();
getActivity().setProgressBarVisibility(false);
showProgressDialog(getActivity(), this, "Fetching commands...");
}
@Override
protected void onActivityDetached() {
if (mProgress != null) {
mProgress.dismiss();
mProgress = null;
}
}
@Override
protected void onActivityAttached() {
if (mProgress == null) {
showProgressDialog(getActivity(), this, "Fetching commands...");
}
}
@Override
protected List<CommandInfo> onResponse(String response) {
JSONObject insertNameHere = null;
try {
insertNameHere = new JSONObject(response);
} catch (JSONException e) {
e.printStackTrace();
return null;
}
try {
JSONArray cmds = insertNameHere.getJSONArray("cmds");
ImmutableList.Builder<CommandInfo> commandListBuilder = ImmutableList.builder();
for (int i = 0; i < cmds.length(); i++) {
commandListBuilder.add(new CommandInfo(cmds.getJSONObject(i)));
}
return commandListBuilder.build();
} catch (JSONException e) {
e.printStackTrace();
return null;
}
}
@Override
protected void onCancelled(List<CommandInfo> error) {
super.onCancelled(error);
dismissProgressDialog();
}
@Override
protected void onPostExecute(List<CommandInfo> commands) {
super.onPostExecute(commands);
ImmutableSortedSet.Builder<String> tags = ImmutableSortedSet.naturalOrder();
SortedSetMultimap<String, String> tagCommandNames = TreeMultimap.create();
SortedSetMultimap<String, CommandInfo> commandNameInfos = TreeMultimap.create();
tags.add(CommandHolder.COMMONLY_USED_COMMANDS_TAG);
for (CommandInfo cmdInfo : commands) {
String cmdName = cmdInfo.getName();
if (CommandHolder.isCommonlyUsedCommand(cmdName)) {
tagCommandNames.put(CommandHolder.COMMONLY_USED_COMMANDS_TAG, cmdName);
}
for (String tag : cmdInfo.getTags()) {
tags.add(tag);
tagCommandNames.put(tag, cmdName);
}
commandNameInfos.put(cmdName, cmdInfo);
}
CommandHolder.setTags(tags.build());
CommandHolder.setTagCommandNames(tagCommandNames);
CommandHolder.setCommandInfos(commandNameInfos);
getActivity().updateCommandExpandableMenu();
dismissProgressDialog();
}
};
}
private HermitHttpServerRequest<Void> newSaveHistoryRequest() {
return new HermitHttpServerRequest<Void>(mConsole, HttpRequest.POST) {
@Override
protected void onPreExecute() {
super.onPreExecute();
getActivity().disableInput(false);
}
@Override
protected Void onResponse(String response) {
JSONObject history = null;
try {
history = new JSONObject(response);
} catch (JSONException e) {
e.printStackTrace();
}
String path = "";
if (Prefs.isHistoryDirCustom(getActivity())) {
path = Prefs.getHistoryDir(getActivity());
} else {
path = DeviceConstants.CACHE_DIR;
}
final File file = new File(path + HISTORY_FILENAME);
if (file.exists()) {
JsonUtils.saveJsonFile(history, file.getAbsolutePath());
} else {
cancel(true);
}
return null;
}
@Override
protected void onCancelled(Void error) {
String newErrorMessage = getErrorMessage();
setErrorMessage(null);
if (newErrorMessage != null && getActivity() != null) {
getActivity().addErrorResponseEntry(newErrorMessage);
}
super.onCancelled(error);
}
@Override
protected void onPostExecute(Void nothing) {
super.onPostExecute(nothing);
getActivity().showToast("History saved successfully!");
}
};
}
private void loadHistoryCommands(final List<HistoryCommand> historyCommands) {
loadHistoryCommands(historyCommands, 0);
}
private void loadHistoryCommands(final List<HistoryCommand> historyCommands, final int index) {
if (!historyCommands.isEmpty()) {
Command tokenCommand = new Command(mToken, historyCommands.get(index).getCommand());
new HermitHttpServerRequest<CommandResponse>(mConsole, HttpRequest.POST) {
@Override
protected void onPreExecute() {
super.onPreExecute();
getActivity().disableInput(false);
}
@Override
protected CommandResponse onResponse(String response) {
try {
return new CommandResponse(new JSONObject(response));
} catch (JSONException e) {
e.printStackTrace();
return null;
}
}
@Override
protected void onCancelled(CommandResponse error) {
String newErrorMessage = getErrorMessage();
setErrorMessage(null);
if (newErrorMessage != null && getActivity() != null) {
getActivity().addErrorResponseEntry(newErrorMessage);
}
super.onCancelled(error);
}
@Override
protected void onPostExecute(CommandResponse response) {
super.onPostExecute(response);
mToken.setAst(response.getAst());
if (index < historyCommands.size() - 1) {
loadHistoryCommands(historyCommands, index + 1);
} else {
getActivity().setCommandHistory(historyCommands);
getActivity().addErrorResponseEntry("Session loaded successfully!");
}
}
}.execute(mServerUrl + "/command", tokenCommand.toString());
}
}
private HermitHttpServerRequest<CommandResponse> newRunCommandRequest(final String command) {
return new HermitHttpServerRequest<CommandResponse>(mConsole, HttpRequest.POST) {
@Override
protected CommandResponse doInBackground(String... params) {
return super.doInBackground(params);
}
@Override
protected CommandResponse onResponse(String response) {
try {
return new CommandResponse(new JSONObject(response));
} catch (JSONException e) {
e.printStackTrace();
return null;
}
}
@Override
protected void onPostExecute(CommandResponse response) {
super.onPostExecute(response);
int fromAst = mToken.getAst();
int toAst = response.getAst();
mToken.setAst(toAst);
getActivity().addCommandHistoryEntry(fromAst, command, toAst);
getActivity().appendCommandResponse(response);
}
};
}
private HermitHttpServerRequest<String> newRunAbortResumeRequest() {
return new HermitHttpServerRequest<String>(mConsole, HttpRequest.POST) {
@Override
protected String onResponse(String response) {
try {
return new JSONObject(response).getString("msg");
} catch (JSONException e) {
e.printStackTrace();
return null;
}
}
@Override
protected void onPostExecute(String message) {
super.onPostExecute(message);
mToken = null;
getActivity().clearCommandHistory();
getActivity().appendErrorResponse(message);
CommandHolder.setTags(null);
CommandHolder.setTagCommandNames(null);
CommandHolder.setCommandInfos(null);
getActivity().updateCommandExpandableMenu();
}
};
}
public void runDelayedRequest() {
if (mDelayedRequestName != null) {
switch (mDelayedRequestName) {
case COMMAND: {
String input = mTempBundle.getString("input");
int charsPerLine = mTempBundle.getInt("charsPerLine");
runCommand(input, charsPerLine);
mTempBundle.remove("input");
mTempBundle.remove("charsPerLine");
break;
}
case COMMANDS: {
fetchCommands();
break;
}
case COMPLETE: {
String input = mTempBundle.getString("input");
completeInput(input);
mTempBundle.remove("input");
break;
}
case CONNECT: {
connect(mServerUrl);
break;
}
case HISTORY: {
fetchHistory();
break;
}
default:
break;
}
}
}
void attachConsole(ConsoleActivity console) {
mConsole = console;
}
private void dismissProgressDialog() {
if (mProgress != null) {
mProgress.dismiss();
}
}
public int getAst() {
return (mToken != null) ? mToken.getAst() : NO_TOKEN;
}
private boolean isNetworkConnected(RequestName name) {
if (Prefs.isBluetoothSource(mConsole)) {
if (BluetoothUtils.isBluetoothEnabled(mConsole)) {
if (BluetoothUtils.getBluetoothDevice(mConsole) != null) {
return true;
} else {
notifyDelay(name);
BluetoothUtils.findDeviceName(mConsole);
}
} else {
notifyDelay(name);
BluetoothUtils.enableBluetooth(mConsole);
}
} else if (Prefs.isWebSource(mConsole)) {
if (InternetUtils.isAirplaneModeOn(mConsole)) {
mConsole.appendErrorResponse("ERROR: Please disable airplane mode before attempting to connect.");
} else if (!InternetUtils.isWifiConnected(mConsole)
&& !InternetUtils.isMobileConnected(mConsole)) {
notifyDelay(name);
InternetUtils.enableWifi(mConsole);
} else {
return true;
}
}
return false;
}
public boolean isRequestDelayed() {
return !mDelayedRequestName.equals(RequestName.NULL);
}
public boolean isTokenAcquired() {
return isTokenAcquired(false);
}
private boolean isTokenAcquired(boolean complainIfNot) {
if (mToken == null) {
if (complainIfNot) {
mConsole.appendErrorResponse("ERROR: No token (connect to server first).");
}
return false;
}
return true;
}
private void notifyDelay(RequestName name) {
mDelayedRequestName = name;
}
public void notifyDelayedRequestFinished() {
mDelayedRequestName = RequestName.NULL;
}
private void showProgressDialog(Context context, final AsyncTask<?,?,?> task, String message) {
mProgress = new ProgressDialog(context);
mProgress.setProgressStyle(ProgressDialog.STYLE_SPINNER);
mProgress.setMessage(message);
mProgress.setCancelable(true);
mProgress.setOnCancelListener(new OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
task.cancel(true);
}
});
mProgress.show();
}
private enum RequestName {
COMMAND, COMMANDS, COMPLETE, CONNECT, HISTORY, NULL
}
public static final Parcelable.Creator<HermitClient> CREATOR
= new Parcelable.Creator<HermitClient>() {
@Override
public HermitClient createFromParcel(Parcel in) {
return new HermitClient(in);
}
@Override
public HermitClient[] newArray(int size) {
return new HermitClient[size];
}
};
private HermitClient(Parcel in) {
mDelayedRequestName = ParcelUtils.readEnum(in);
mServerUrl = in.readString();
mTempBundle = in.readBundle();
mToken = in.readParcelable(Token.class.getClassLoader());
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
ParcelUtils.writeEnum(dest, mDelayedRequestName);
dest.writeString(mServerUrl);
dest.writeBundle(mTempBundle);
dest.writeParcelable(mToken, flags);
}
}